/// /// /// import { useStatusBar, openInTheSameTab, encodeTitleURI, } from "../scrapbox-userscript-std/dom.ts"; import type { Scrapbox, SearchedTitle } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts"; declare const scrapbox: Scrapbox; const id = "next-action"; let initialized: Promise; const dummyImage = "/assets/img/favicon/apple-touch-icon.png"; export function setup(projects: string[]) { const selector = `head style[data-userscript-name="${id}"]`; document.querySelector(selector)?.remove?.(); const style = document.createElement("style"); style.dataset.userscriptName = id; style.textContent = `a#${id}.tool-btn:hover { text-decoration: none; } a#${id}.tool-btn::before { position: absolute; content: "\\f0ae"; font: 900 20px/46px "Font Awesome 5 Free"; } a#${id}.tool-btn img { opacity: 0; } a#${id}.tool-btn ~ ul a::before { position: absolute; font-family: "Font Awesome 5 Free"; font-weight: 900; } a#${id}.tool-btn ~ ul img { opacity: 0; margin-right: 0; }`; document.head.append(style); if (!document.getElementById(id)) { scrapbox.PageMenu.addMenu({ title: id, image: dummyImage, onClick: async () => { initialized ??= load(projects); await initialized; }, }); } } async function load(projects: string[]) { scrapbox.PageMenu(id).removeAllItems(); const { render, dispose } = useStatusBar(); let count = 0; try { for (const project of projects) { render( { type: "spinner" }, { type: "text", text: `Searching "/${project}" for next actions...`}, ); for await (const title of listNextActions(project)) { count++; scrapbox.PageMenu(id).addItem({ title, onClick: () => { const path = `https://scrapbox.io/${ project }/${encodeTitleURI(title)}`; if (project !== scrapbox.Project.name) { window.open(path); return; } openInTheSameTab(path); }, }); } if (project === projects[projects.length - 1]) continue; scrapbox.PageMenu(id).addSeparator(); } render( { type: "check-circle" }, { type: "text", text: `Found ${count} actions.`}, ); } catch(e: unknown) { render( { type: "exclamation-triangle" }, { type: "text", text: e instanceof Error ? `${e.name} ${e.message}` : `Unknown error! (see developper console)`, }, ); console.error(e); } finally { setTimeout(() => dispose(), 1000); } } async function* listNextActions(project: string, filter?: RegExp) { filter ??= /^(?:⬜(?:[^p]*|p[^-]*))|🔳/; if (project === scrapbox.Project.name) { for (const { title, exists } of scrapbox.Project.pages) { if (!filter.test(title)) continue; yield title; } return; } const titles = new Set(); for await (const title of getLinks(project)) { if (!titles.has(title) && filter.test(title)) { titles.add(title); yield title; } } } async function* getLinks(project: string) { const path = `/api/pages/${project}/search/titles`; let followingId = null; do { const path_ = `${path}${ followingId ? `?followingId=${followingId}` : "" }` as string; const res = await fetch(path_); followingId = res.headers.get("X-following-id"); const pages = (await res.json()) as SearchedTitle[]; for (const { title, links } of pages) { yield title; for (const link of links) { yield link; } } if (!followingId) break; // 空文字列の場合もある } while (true) }